// This source is subject to the Microsoft Public License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using System.Collections;
using System.Configuration;
using System.Globalization;
using System.Reflection;
using System.Resources;
using System.Security.Principal;
using System.Web; // For tracing
using System.ComponentModel;
using Microsoft.Win32;

using QuoteAdvantage.CrmSdk;
using QuoteAdvantage.CrmSdk.Discovery;


namespace QuoteAdvantage
{
   /// <summary>
   /// Quoting engine that handles product configuration tasks communications with Microsoft CRM
   /// </summary>
   public class Engine
   {
      protected string m_strWebServiceUrl;
      protected CrmService m_objService = null;
      protected ICredentials m_objCredentials = null;
      protected WhoAmIResponse m_objUserIdentity = null;
      protected CrmImpersonator objCrmImpersonator = null;

      protected DateTime m_dtPerformanceTimer;
      protected string m_strConfiguratorFolder;
      protected ConfiguratorSettings m_objSettings = new ConfiguratorSettings();
      protected Hashtable m_objProductIdCache = new Hashtable(100);    // Products cached by Product ID
      protected Hashtable m_objProductNumberCache = new Hashtable(100);    // Products cached by Product Number
      protected Hashtable m_objQuoteDetailCache = new Hashtable(100);
      protected Hashtable m_objUnitOfMeasureCache = new Hashtable(50);

      protected TraceContext m_objWebTrace = null;
      protected ResourceManager m_objResourceManager;
      protected string m_strProductCustomField1 = "";
      protected string m_strProductCustomField2 = "";

      public string ProductCustomField1
      {
         get { return m_strProductCustomField1; }
      }

      public string ProductCustomField2
      {
         get { return m_strProductCustomField2; }
      }

      public Engine(ResourceManager objResourceManager)
      {
         m_objResourceManager = objResourceManager;
         Init();
      }

      public Engine(TraceContext trace, ResourceManager objResourceManager)
      {
         WebTrace = trace;
         m_objResourceManager = objResourceManager;
         Init();
      }

      private void Init()
      {
         Assembly objSelf = Assembly.GetExecutingAssembly();

         string strPath = objSelf.CodeBase.Replace("file:///", "");
         FileInfo fi = new FileInfo(strPath);
         m_strConfiguratorFolder = fi.DirectoryName;
         m_objSettings.LoadSettings(m_strConfiguratorFolder + "\\CategoryList.xml");
         Connect();

         // Load the names of any custom fields that need to be included
         if (ConfigurationManager.AppSettings["ProductCustomData1"] != null)
         {
            m_strProductCustomField1 = ConfigurationManager.AppSettings["ProductCustomData1"];
         }

         if (ConfigurationManager.AppSettings["ProductCustomData2"] != null)
         {
            m_strProductCustomField2 = ConfigurationManager.AppSettings["ProductCustomData2"];
         }
      }

      public TraceContext WebTrace
      {
         get { return m_objWebTrace; }
         set { m_objWebTrace = value; }
      }

      // Add a parent product to a quote at the root level
      public QuoteDetail AddProduct(Quote objQuote, Product objProduct)
      {
         QuoteDetail objQuoteDetail = new QuoteDetail();

         objQuoteDetail.QuoteDetailId = Guid.NewGuid();
         objQuoteDetail.Init(objQuote, objProduct);
         objQuoteDetail.LineItemNumber = objQuote.LineItems.Count;

         DebugMessage("AddProduct(Quote, Product, CategoryId)\r\n");

         try
         {
            quotedetail objNew = objQuoteDetail.CreateCrmObject();
            m_objService.Create(objNew);
            objQuote.LineItems.Add(objQuoteDetail);
            this.AddParentProductLinkedItems(objQuote, objQuoteDetail);
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);

            ProductConfigException ex = new ProductConfigException("Unable to Add Product. " + strMessage, err);
            throw ex;
         }

         return objQuoteDetail;
      }


      // Add a child line-item to an existing item on a quote
      public QuoteDetail AddChildProduct(Quote objQuote, QuoteDetail objParentItem, Product objProduct, string strCategoryId, Guid unitOfMeasureId)
      {
         DebugMessage("AddChildProduct()\r\n");

         QuoteDetail objQuoteDetail = new QuoteDetail();
         objQuoteDetail.QuoteDetailId = Guid.NewGuid();
         objQuoteDetail.Init(objQuote, objParentItem, objProduct);
         objQuoteDetail.CategoryId = strCategoryId;
         objQuoteDetail.Quantity = objParentItem.Quantity;
         objQuoteDetail.LineItemNumber = objQuote.LineItems.Count;
         objQuoteDetail.AllowDiscounting = true;  // This should be loaded from an XML file

         if (unitOfMeasureId != Guid.Empty)
         {
            // NOTE: A UOM reference that is not defined on this server will trigger an exception
            objQuoteDetail.UnitOfMeasureId = unitOfMeasureId;
         }

         try
         {
            quotedetail objNew = objQuoteDetail.CreateCrmObject();
            m_objService.Create(objNew);
            objQuote.LineItems.Add(objQuoteDetail);
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            // NOTE: A UOM reference that is not defined on this server will trigger an exception
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);
            throw new ApplicationException("Unable to Add Product. " + strMessage);
         }

         return objQuoteDetail;
      }

      // Used for generating test data. This function will add a new product to the CRM Product Catalog.
      public Product AddProductCatalogEntry(Product objProduct)
      {
         DebugMessage("AddProductCatalogEntry()\r\n");

         try
         {
            objProduct.ProductId = Guid.NewGuid();
            product objCrmProduct = objProduct.CreateCrmObject();
            m_objService.Create(objCrmProduct);
         }
         catch (System.Web.Services.Protocols.SoapException ex)
         {
            string strMessage = String.Concat("ErrorMessage: ", ex.Message, " ", ex.Detail.OuterXml, " Source: ", ex.Source);
            throw new ApplicationException("Unable to Add Product Catalog Entry. " + strMessage);
         }

         return objProduct;
      }

      private void AddParentProductLinkedItems(Quote objQuote, QuoteDetail objParentItem)
      {
         DebugMessage("AddParentProductLinkedItems() Parent Product = " + objParentItem.ProductNumber);

         try
         {
            XmlNode nodeCategoryTree = LoadProductCategoryList();

            if (nodeCategoryTree != null)
            {
               XmlNode nodeCategory = nodeCategoryTree.SelectSingleNode("//Category[@categoryId='" + objParentItem.CategoryId + "']");

               if (nodeCategory != null)
               {
                  foreach (XmlNode objItem in nodeCategory.ChildNodes)
                  {
                     if (objItem.NodeType == XmlNodeType.Element)
                     {
                        if (objItem.Name == "Item")
                        {
                           if (objItem.Attributes["productId"].Value == objParentItem.ProductNumber)
                           {
                              foreach (XmlNode objRule in objItem.ChildNodes)
                              {
                                 if (objRule.Name == "LinkedItem")
                                 {
                                    AddParentProductLinkedItem(objRule, nodeCategory, objQuote, objParentItem);
                                 }
                              }
                           }
                        }
                     }
                  } // end foreach
               }
            }
         }
         catch (Exception ex)
         {
            throw new ApplicationException("Error while adding linked items for parent product " + objParentItem.ProductNumber + " from CategoryList.xml", ex);
         }
      }

      private void AddParentProductLinkedItem(XmlNode objRule, XmlNode nodeCategory, Quote objQuote, QuoteDetail objParentItem)
      {
         // We need to add this item
         string strLinkedProductId = objRule.InnerText;

         DebugMessage("AddParentProductLinkedItem() Parent Product = " + objParentItem.ProductNumber + ", LinkedItem = " + strLinkedProductId);

         // First check and make sure that it's not already there
         string strLinkedItemCategoryName = objParentItem.CategoryId + " LinkedItem For " + objParentItem.ProductId;
         QuoteDetail objExisting = objParentItem.FindChildItem(strLinkedProductId, strLinkedItemCategoryName);

         if (objExisting == null)
         {
            Product objLinkedProduct = LoadProductFromProductNumber(strLinkedProductId, objParentItem.QuoteOwner.PriceLevelId);

            if (objLinkedProduct.ProductId == Guid.Empty)
            {
               throw new ApplicationException("Error while adding linked item for parent product " + objParentItem.ProductNumber + ".  Unable to load linked item with Product ID:  " + strLinkedProductId);
            }
            string strLinkedUom = objRule.Attributes["uom"].Value;
            Guid guidLinkedUom;

            if (strLinkedUom == "")
            {
               guidLinkedUom = objLinkedProduct.DefaultUnitOfMeasureId;
            }
            else
            {
               guidLinkedUom = new Guid(strLinkedUom);
            }

            // Determine Allow Discounting status

            //        bool bAllowDiscounting = AllowLinkedItemDiscounting()


            QuoteDetail objNewItem = this.AddChildProduct(objQuote, objParentItem, objLinkedProduct, strLinkedItemCategoryName, guidLinkedUom);
         }
      }

      // Add all of the linked items for a child product that are required by a child product in a specific category. 
      // It's possible that these items might already be present on the quote
      private void AddLinkedItems(Quote objQuote, QuoteDetail objParentItem, QuoteDetail objProduct)
      {
         try
         {
            XmlNode objNodeRoot = LoadCategoryList(objParentItem);

            if (objNodeRoot != null)
            {
               foreach (XmlNode objCategory in objNodeRoot.ChildNodes)
               {
                  if (objCategory.Attributes["categoryId"].Value == objProduct.CategoryId)
                  {
                     foreach (XmlNode objItem in objCategory.ChildNodes)
                     {
                        if (objItem.NodeType == XmlNodeType.Element)
                        {
                           if (objItem.Name == "Item")
                           {
                              if (objItem.Attributes["productId"].Value == objProduct.ProductNumber)
                              {
                                 foreach (XmlNode objRule in objItem.ChildNodes)
                                 {
                                    if (objRule.Name == "LinkedItem")
                                    {
                                       AddLinkedItem(objRule, objQuote, objParentItem, objProduct);
                                    }
                                 }
                              }
                           }
                        }
                     }
                  }
               }
            }
         }
         catch (Exception ex)
         {
            throw new ApplicationException("Error while adding linked items for " + objParentItem.ProductNumber + " from " + objParentItem.ConfigRuleFileName, ex);
         }
      }

      private void AddLinkedItem(XmlNode objRule, Quote objQuote, QuoteDetail objParentItem, QuoteDetail objProduct)
      {
         // We need to add this item
         string strLinkedProductId = objRule.InnerText;

         DebugMessage("AddLinkedItem() Parent Product = " + objProduct.ProductNumber + ", LinkedItem = " + strLinkedProductId);

         // First check and make sure that it's not already there
         string strLinkedItemCategoryName = objProduct.CategoryId + " LinkedItem For " + objProduct.ProductId;
         QuoteDetail objExisting = objParentItem.FindChildItem(strLinkedProductId, strLinkedItemCategoryName);

         if (objExisting == null)
         {
            Product objLinkedProduct = LoadProductFromProductNumber(strLinkedProductId, objParentItem.QuoteOwner.PriceLevelId);

            string strLinkedUom = objRule.Attributes["uom"].Value;
            Guid guidLinkedUom;

            if (strLinkedUom == "")
            {
               guidLinkedUom = objLinkedProduct.DefaultUnitOfMeasureId;
            }
            else
            {
               guidLinkedUom = new Guid(strLinkedUom);
            }

            QuoteDetail objNewItem = this.AddChildProduct(objQuote, objProduct, objLinkedProduct, strLinkedItemCategoryName, guidLinkedUom);
         }
      }

      // Add all of the child products that are flagged as auto-add item(s) within their category
      // Single select categories can have one item, multi-select categories may multiple items
      // If an item is auto-added that could have nested children, then also search for auto-add nested children
      public void AutoAddChildProducts(Quote objQuote, QuoteDetail objParentProduct, Organization objOrganization)
      {
         try
         {
            XmlNode objNodeRoot = LoadCategoryList(objParentProduct);

            if (objNodeRoot != null)
            {
               foreach (XmlNode nodeCategory in objNodeRoot.ChildNodes)
               {
                  if (nodeCategory.NodeType == XmlNodeType.Element)
                  {
                     string strCategoryId = nodeCategory.Attributes["categoryId"].Value;
                     bool bMultiSelect = false;

                     if (nodeCategory.Attributes["type"] != null)
                     {
                        string strType = nodeCategory.Attributes["type"].Value;

                        if (strType == "MultiSelect")
                           bMultiSelect = true;
                     }

                     bool bItemAdded = false;

                     foreach (XmlNode nodeItem in nodeCategory.ChildNodes)
                     {
                        if (bMultiSelect || !bItemAdded)  // Add either one single item or several multi-select items
                        {
                           if (nodeItem.NodeType == XmlNodeType.Element)
                           {
                              if (nodeItem.Attributes["autoAdd"] != null)
                              {
                                 bool bAutoAdd = false;

                                 try
                                 {
                                    string strValue = nodeItem.Attributes["autoAdd"].Value;
                                    bAutoAdd = Convert.ToBoolean(strValue);
                                 }
                                 catch (Exception ex)
                                 {
                                    throw new Exception("Invalid auto-add value in category: " + strCategoryId, ex);
                                 }

                                 if (bAutoAdd == true)
                                 {
                                    string strPartNumber = nodeItem.Attributes["productId"].Value;
                                    QuoteDetail objNewItem = AddChildProduct(strPartNumber, strCategoryId, objNodeRoot, objQuote, objParentProduct, objOrganization);
                                    bItemAdded = true;

                                    if (nodeItem.Attributes["ruleFileName"] != null)
                                    {
                                       string strRuleFileName = nodeItem.Attributes["ruleFileName"].Value;

                                       if (strRuleFileName != "")
                                       {
                                          // If this item has nested children, then we might need to do a deeper
                                          // level of auto-adding.
                                          AutoAddChildProducts(objQuote, objNewItem, objOrganization);
                                       }
                                    }
                                 }
                              }
                           }
                        }
                     }
                  }
               } // end foreach
            }
         }
         catch (Exception ex)
         {
            throw new ApplicationException("Error while auto-adding products from " + objParentProduct.ConfigRuleFileName, ex);
         }
      }

      public QuoteDetail AddChildProduct(string strProductNumber, string strCategoryId, XmlNode objNodeRoot, Quote objQuote, QuoteDetail objParentProduct, Organization objOrganization)
      {
         QuoteDetail objNewItem = null;
         Product objProduct = this.LoadProductFromProductNumber(strProductNumber, objQuote.PriceLevelId);

         objProduct.CategoryId = strCategoryId;

         XmlNode nodeProduct = this.GetProductNode(objNodeRoot, strCategoryId, objProduct.ProductNumber);

         if (nodeProduct.Attributes["allowDiscounting"] != null)
         {
            objProduct.AllowDiscounting = Convert.ToBoolean(nodeProduct.Attributes["allowDiscounting"].Value);
         }

         ApplyUnitOfMeasure(objNodeRoot, objProduct, objOrganization);

         if (objProduct != null)
         {
            objNewItem = this.AddChildProduct(objQuote, objParentProduct, objProduct, strCategoryId, objProduct.UnitOfMeasureId);
         }

         return objNewItem;
      }


      // Modify the quantity of a parent item and all of its child items
      public void ChangeParentItemQty(Quote objQuote, string strQuoteDetailId, int nNewQty)
      {
         QuoteDetail objParentItem = objQuote.FindQuoteDetail(new Guid(strQuoteDetailId));

         ArrayList objChildList = objParentItem.GetChildItemList();

         objParentItem.Quantity = nNewQty;
         UpdateQuoteDetail(objParentItem);

         foreach (QuoteDetail objChild in objChildList)
         {
            // Recursively update all children
            ChangeParentItemQty(objQuote, objChild.QuoteDetailId.ToString(), nNewQty);
         }
      }

      public string GetRuleFileName(QuoteDetail objParentItem)
      {
         DebugMessage("GetRuleFileName() 1");

         string strRuleFileName = "";

         if (objParentItem.ConfigParentId == Guid.Empty)
         {
            // Need a rule file for a parent item
            strRuleFileName = objParentItem.ConfigRuleFileName;

            if (strRuleFileName == "")
            {
               XmlNode nodeCategoryTree = LoadProductCategoryList();
               XmlNode nodeCategory = nodeCategoryTree.SelectSingleNode("//Category[@categoryId='" + objParentItem.CategoryId + "']");

               if (nodeCategory != null)
               {
                  XmlNode nodeItem = nodeCategory.SelectSingleNode("Item[@productId='" + objParentItem.ProductNumber + "']");

                  if (nodeItem != null)
                  {
                     XmlAttribute attr = nodeItem.Attributes["ruleFileName"];

                     if (attr != null)
                     {
                        strRuleFileName = attr.Value;
                        objParentItem.ConfigRuleFileName = strRuleFileName;
                     }
                  }
               }
            }
         }
         else
         {
            // Find the name of the rule file for a nested item

            // First, we need the category list for this item's parent
            QuoteDetail objGrandParent = objParentItem.QuoteOwner.FindQuoteDetail(objParentItem.ConfigParentId);
            XmlNode objCatNode = LoadCategoryList(objGrandParent);

            // Search the parent's rule file for the name of the nested rule file
            XmlNode objProductNode = GetProductNode(objCatNode, objParentItem.CategoryId, objParentItem.ProductNumber);

            if (objProductNode.Attributes["ruleFileName"] != null)
            {
               strRuleFileName = objProductNode.Attributes["ruleFileName"].Value;
            }
         }

         DebugMessage("GetRuleFileName() 2");

         return strRuleFileName;
      }

      public Organization LoadOrganization()
      {
         DebugMessage("LoadOrganization()");
         Organization objOrg = null;

         using (new Microsoft.Crm.Sdk.CrmImpersonator())
         {
            TargetRetrieveBusinessUnit targetGetBusinessUnit = new TargetRetrieveBusinessUnit();
            targetGetBusinessUnit.EntityId = m_objUserIdentity.BusinessUnitId;
            RetrieveRequest getBusinessUnit = new RetrieveRequest();
            getBusinessUnit.Target = targetGetBusinessUnit;
            getBusinessUnit.ColumnSet = new AllColumns();
            RetrieveResponse businessUnitResponse = (RetrieveResponse)m_objService.Execute(getBusinessUnit);
            businessunit objBusinessUnit = (businessunit)businessUnitResponse.BusinessEntity;

            ColumnSet objColumns = new ColumnSet();
            objColumns.Attributes = new string[] { "currencyformatcode", "currencysymbol", "organizationid" };
            organization objCrmOrganization = (organization)m_objService.Retrieve(EntityName.organization.ToString(), objBusinessUnit.organizationid.Value, objColumns);
            objOrg = new Organization();
            objOrg.LoadFromCrmOrganization(objCrmOrganization);
         }

         return objOrg;
      }

      public Quote LoadQuote(Guid quoteId)
      {
         Quote objQuote = new Quote();

         try
         {
            DebugMessage("LoadQuote('" + quoteId.ToString() + "')");
            LoadQuoteHeader(quoteId, objQuote);
            LoadLineItems(objQuote);
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);
            throw new ApplicationException("Unable to load quote.  " + strMessage);
         }

         return objQuote;
      }

      public XmlNode LoadProductCategoryList()
      {
         DebugMessage("Engine.LoadProductCategoryList() 1");
         XmlDocument docXml = new XmlDocument();

         docXml.Load(m_strConfiguratorFolder + "\\" + "CategoryList.xml");
         XmlNode nodeRoot = docXml.DocumentElement;
         XmlNode nodeProductCategory = null;

         foreach (XmlNode node in nodeRoot.ChildNodes)
         {
            if (node.Name == "CategoryList")
            {
               nodeProductCategory = node;
            }
         }

         DebugMessage("Engine.LoadProductCategoryList() 2");

         return nodeProductCategory;
      }


      public XmlNode LoadCategoryList(QuoteDetail objParentItem)
      {
         DebugMessage("LoadCategoryList() 1");

         XmlNode nodeRoot = null;

         string strRuleFileName = GetRuleFileName(objParentItem);

         if (strRuleFileName != "")
         {
            XmlDocument docXml = new XmlDocument();

            docXml.Load(m_strConfiguratorFolder + "\\" + strRuleFileName);
            nodeRoot = docXml.DocumentElement;
         }

         DebugMessage("LoadCategoryList() 2");

         return nodeRoot;
      }

      public ArrayList LoadCategoryItems(string strCategoryId, Guid guidPriceLevelId)
      {
         ArrayList resultList = new ArrayList();

         XmlDocument docXml = new XmlDocument();

         docXml.Load(m_strConfiguratorFolder + "\\" + "CategoryList.xml");
         XmlNode nodeRoot = docXml.DocumentElement;
         XmlNode nodeCategory = docXml.SelectSingleNode("//Category[@categoryId='" + strCategoryId + "']");

         if (nodeCategory != null)
         {
            foreach (XmlNode node in nodeCategory.ChildNodes)
            {
               if (node.Name == "Item")
               {
                  if (node.Attributes["productId"] == null)
                  {
                     throw new ApplicationException("Error processing CategoryList.xml file. 'productId' attribute was not found.");
                  }

                  try
                  {
                     string strProductId = node.Attributes["productId"].Value;

                     Product objProduct = LoadProductFromProductNumber(strProductId, guidPriceLevelId);
                     if (objProduct != null)
                     {
                        objProduct.CategoryId = strCategoryId;
                        objProduct.AllowDiscounting = true;

                        if (node.Attributes["allowDiscounting"] != null)
                        {
                           objProduct.AllowDiscounting = Convert.ToBoolean(node.Attributes["allowDiscounting"].Value);
                        }

                        resultList.Add(objProduct);
                     }
                  }
                  catch (Exception ex)
                  {
                     throw new ApplicationException("Error processing CategoryList.xml file.", ex);
                  }
               }
            } // end for
         }

         return resultList;
      }

      public Product LoadProduct(Guid productId, Guid priceLevelId)
      {
         Product objProduct = (Product)m_objProductIdCache[productId];

         if (objProduct == null)
         {
            try
            {
               DebugMessage("LoadProduct('" + productId.ToString() + "," + priceLevelId.ToString() + "')");

               using (new Microsoft.Crm.Sdk.CrmImpersonator())
               {
                  ColumnSet objColumns = new ColumnSet();
                  objColumns.Attributes = new string[] {"defaultuomid", "description", "iskit", "name", "price", 
                                                      "productid", "productnumber", "producttypecode", "producturl", "standardcost", 
                                                      "name", "name"};

                  if (m_strProductCustomField1 != "")
                     objColumns.Attributes[10] = m_strProductCustomField1;

                  if (m_strProductCustomField1 != "")
                     objColumns.Attributes[11] = m_strProductCustomField2;

                  // Create a ConditionExpression.
                  ConditionExpression conditionPrincipal1 = new ConditionExpression();

                  conditionPrincipal1.AttributeName = "productid";
                  conditionPrincipal1.Operator = ConditionOperator.Equal;
                  conditionPrincipal1.Values = new object[1];
                  conditionPrincipal1.Values[0] = productId;

                  // Create the FilterExpression.
                  FilterExpression filterPrincipal = new FilterExpression();

                  // Set the properties of the FilterExpression.
                  filterPrincipal.FilterOperator = LogicalOperator.And;
                  filterPrincipal.Conditions = new ConditionExpression[] { conditionPrincipal1 };

                  // Create the QueryExpression.
                  QueryExpression queryPrincipal = new QueryExpression();

                  // Set the properties of the QueryExpression.
                  queryPrincipal.EntityName = EntityName.product.ToString();
                  queryPrincipal.ColumnSet = objColumns;
                  queryPrincipal.Criteria = filterPrincipal;

                  BusinessEntityCollection objList = m_objService.RetrieveMultiple(queryPrincipal);

                  if (objList.BusinessEntities.Length > 0)
                  {
                     objProduct = new Product();
                     objProduct.LoadFromCrmProduct((product)objList.BusinessEntities[0], m_strProductCustomField1, m_strProductCustomField2);

                     ProductPriceLevel objPrice = LoadProductPriceLevel(priceLevelId, objProduct.ProductId);

                     if (objPrice == null)
                     {
                        objProduct.Price = objProduct.ListPrice;
                     }
                     else
                     {
                        objProduct.Price = objPrice.Amount;
                     }
                  }
                  else
                  {
                     objProduct = CreateEmptyProduct(productId.ToString());
                  }

                  // Add to both caches
                  if (null == m_objProductNumberCache[objProduct.ProductNumber.ToUpper()])
                     m_objProductNumberCache.Add(objProduct.ProductNumber.ToUpper(), objProduct);

                  m_objProductIdCache.Add(objProduct.ProductId, objProduct);
               }
            }
            catch (System.Web.Services.Protocols.SoapException err)
            {
               string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);
               throw new ApplicationException("Unable to load Product GUID: " + productId + "Error: " + strMessage);
            }
         }

         return objProduct;
      }

      public Product CreateEmptyProduct(string strProductNumber)
      {
         Product objProduct = new Product();
         objProduct.ProductNumber = strProductNumber;
         objProduct.Name = "Unable to load product " + strProductNumber;
         objProduct.Description = "Unable to load product " + strProductNumber;
         objProduct.ListPrice = 0;
         objProduct.Price = 0;
         objProduct.StandardCost = 0;

         return objProduct;
      }

      public ProductPriceLevel LoadProductPriceLevel(Guid priceLevelId, Guid productId)
      {
         DebugMessage("LoadProductPriceLevel('" + priceLevelId + ", " + productId + "')");
         ProductPriceLevel objPrice = null;

         using (new Microsoft.Crm.Sdk.CrmImpersonator())
         {
            ColumnSet objColumns = new ColumnSet();
            objColumns.Attributes = new string[] { "amount", "pricelevelid", "productid" };

            // Create a ConditionExpression.
            ConditionExpression conditionPrincipal1 = new ConditionExpression();

            conditionPrincipal1.AttributeName = "productid";
            conditionPrincipal1.Operator = ConditionOperator.Equal;
            conditionPrincipal1.Values = new object[1];
            conditionPrincipal1.Values[0] = productId;

            ConditionExpression conditionPrincipal2 = new ConditionExpression();
            conditionPrincipal2.AttributeName = "pricelevelid";
            conditionPrincipal2.Operator = ConditionOperator.Equal;
            conditionPrincipal2.Values = new object[1];
            conditionPrincipal2.Values[0] = priceLevelId;

            // Create the FilterExpression.
            FilterExpression filterPrincipal = new FilterExpression();

            // Set the properties of the FilterExpression.
            filterPrincipal.FilterOperator = LogicalOperator.And;
            filterPrincipal.Conditions = new ConditionExpression[] { conditionPrincipal1, conditionPrincipal2 };

            // Create the QueryExpression.
            QueryExpression queryPrincipal = new QueryExpression();

            // Set the properties of the QueryExpression.
            queryPrincipal.EntityName = EntityName.productpricelevel.ToString();
            queryPrincipal.ColumnSet = objColumns;
            queryPrincipal.Criteria = filterPrincipal;

            BusinessEntityCollection objList = m_objService.RetrieveMultiple(queryPrincipal);

            if (objList.BusinessEntities.Length > 0)
            {
               objPrice = new ProductPriceLevel();
               objPrice.LoadFromCrmPriceLevel((productpricelevel)objList.BusinessEntities[0]);
            }
         }

         return objPrice;
      }

      public Product LoadProductFromProductNumber(string strProductNumber, Guid guidPriceLevelId)
      {
         DebugMessage("LoadProductProductNumber('" + strProductNumber + "', '" + guidPriceLevelId.ToString() + ")");
         Product objProduct = null;

         objProduct = (Product)m_objProductNumberCache[strProductNumber.ToUpper()];

         if (objProduct == null)
         {
            ColumnSet objColumns = new ColumnSet();
            objColumns.Attributes = new string[] { "defaultuomid", "description", "iskit", "name", "price", "productid", "producturl", "productnumber", "standardcost", "name", "name" };

            if (m_strProductCustomField1 != "")
               objColumns.Attributes[8] = m_strProductCustomField1;

            if (m_strProductCustomField1 != "")
               objColumns.Attributes[9] = m_strProductCustomField2;

            // Create a ConditionExpression.
            ConditionExpression conditionPrincipal = new ConditionExpression();

            // Set the ConditionExpressions properties so that the condition is
            // true when the ownerid of the account equals the principalId.
            conditionPrincipal.AttributeName = "productnumber";
            conditionPrincipal.Operator = ConditionOperator.Equal;
            conditionPrincipal.Values = new object[1];
            conditionPrincipal.Values[0] = strProductNumber;

            // Create the FilterExpression.
            FilterExpression filterPrincipal = new FilterExpression();

            // Set the properties of the FilterExpression.
            filterPrincipal.FilterOperator = LogicalOperator.And;
            filterPrincipal.Conditions = new ConditionExpression[] { conditionPrincipal };

            // Create the QueryExpression.
            QueryExpression queryPrincipal = new QueryExpression();

            // Set the properties of the QueryExpression.
            queryPrincipal.EntityName = EntityName.product.ToString();
            queryPrincipal.ColumnSet = objColumns;
            queryPrincipal.Criteria = filterPrincipal;

            BusinessEntityCollection objList = m_objService.RetrieveMultiple(queryPrincipal);

            if (objList.BusinessEntities.Length > 0)
            {
               objProduct = new Product();
               objProduct.LoadFromCrmProduct((product)objList.BusinessEntities[0], m_strProductCustomField1, m_strProductCustomField2);
               DebugMessage("Product loaded successfully");
            }

            if (objProduct == null)
            {
               objProduct = CreateEmptyProduct(strProductNumber);
            }
            else
            {
               ProductPriceLevel objPrice = LoadProductPriceLevel(guidPriceLevelId, objProduct.ProductId);

               if (objPrice == null)
               {
                  objProduct.Price = objProduct.ListPrice;
                  DebugMessage("Price list item not found. Using ListPrice = " + objProduct.ListPrice);
               }
               else
               {
                  objProduct.Price = objPrice.Amount;
                  DebugMessage("Price list item loaded. Price = " + objPrice.Amount);
               }

               m_objProductNumberCache.Add(objProduct.ProductNumber.ToUpper(), objProduct);
            }
         }
         else
         {
            // The product was found in the cache. We need to return a clone of it.
            objProduct = objProduct.Clone();
            DebugMessage("   Product ID " + strProductNumber + " retrieved from cache.");
         }

         return objProduct;
      }


      public void RemoveAllQuoteDetails(Quote objQuote)
      {
         while (objQuote.LineItems.Count > 0)
         {
            RemoveQuoteDetail((QuoteDetail)objQuote.LineItems[objQuote.LineItems.Count - 1]);
         }
      }

      public void RemoveParentItem(QuoteDetail objParentItem)
      {
         DebugMessage("RemoveParentItem()\r\n");

         if (objParentItem != null)
         {
            ArrayList objChildItemList = objParentItem.GetChildItemList();

            foreach (QuoteDetail item in objChildItemList)
            {
               RemoveQuoteDetail(item);
            }

            RemoveQuoteDetail(objParentItem);
         }
      }

      public void RemoveQuoteDetail(QuoteDetail objItem)
      {
         DebugMessage("RemoveQuoteDetail()\r\n");

         try
         {
            ArrayList objNestedChildren = objItem.GetChildItemList();

            foreach (QuoteDetail objNestedItem in objNestedChildren)
            {
               RemoveQuoteDetail(objNestedItem);
            }

            m_objService.Delete(EntityName.quotedetail.ToString(), objItem.QuoteDetailId);
            objItem.QuoteOwner.LineItems.Remove(objItem);
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);
            throw new ApplicationException("Unable to Remove Product. " + strMessage);
         }
      }


      public void ValidateQuote(Quote objQuote)
      {
         foreach (QuoteDetail objItem in objQuote.LineItems)
         {
            ValidateQuoteDetail(objItem);
         }
      }

      public void ValidateQuoteDetail(QuoteDetail objItem)
      {
         DebugMessage("ValidateQuoteDetail()\r\n");

         if (objItem.ConfigParentId == Guid.Empty)
         {
            string strRuleFileName = GetRuleFileName(objItem);

            if (strRuleFileName != "")
            {
               XmlDocument docXml = new XmlDocument();

               docXml.Load(m_strConfiguratorFolder + "\\" + strRuleFileName);
               XmlNode nodeRoot = docXml.DocumentElement;

               foreach (XmlNode nodeCategory in nodeRoot.ChildNodes)
               {
                  if (nodeCategory.NodeType == XmlNodeType.Element)
                  {
                     ValidateCategory(nodeCategory, objItem);
                  }
               }
            }
         }
      }

      // Search a list of products for a matching product number and category
      private Product FindProduct(ArrayList objProductList, string strProductNumber, string strCategoryName)
      {
         Product objResult = null;

         foreach (Product objProduct in objProductList)
         {
            if (objProduct.ProductNumber.ToUpper() == strProductNumber.ToUpper()
                     && objProduct.CategoryId.ToUpper() == strCategoryName.ToUpper())
            {
               objResult = objProduct;
               break;
            }
         }

         return objResult;
      }

      // Re-configure an item's child line items by adding a list of items where there will be 
      // the appropriate items for each category that's specified. 
      // objSelectedItemList:  ArrayList of Products that have a CategoryId specified
      public void UpdateLineItems(Quote objQuote, QuoteDetail objParentItem, ArrayList objSelectedItemList, Organization objOrganization)
      {
         DebugMessage("UpdateLineItems()");

         // First, remove all child products in the old list that are not in the new list
         ArrayList objChildList = objParentItem.GetChildItemList();

         foreach (QuoteDetail objChild in objChildList)
         {
            Product objMatch = FindProduct(objSelectedItemList, objChild.ProductNumber, objChild.CategoryId);

            if (objMatch == null)
            {
               // We have found a product that is in the old list, but not in the new list
               if (objChild.CategoryId.IndexOf(" LinkedItem For ") == -1)
               {
                  // Only delete this item if it is not a linked item
                  this.RemoveQuoteDetail(objChild);   // Delete the old child product
               }
            }
         }

         // Next, add all of the new products that are not in the old list
         foreach (Product objProduct in objSelectedItemList)
         {
            if (objProduct.ProductNumber != "") // Don't search for empty items
            {
               QuoteDetail objItem = objQuote.FindCategoryItem(objParentItem, objProduct.CategoryId, objProduct.ProductNumber);

               if (objItem == null)
               {
                  // This product hasn't been added yet
                  QuoteDetail objNewItem = this.AddChildProduct(objQuote, objParentItem, objProduct, objProduct.CategoryId, objProduct.UnitOfMeasureId);
                  this.AddLinkedItems(objQuote, objParentItem, objNewItem);
                  this.AutoAddChildProducts(objQuote, objNewItem, objOrganization);
               }
            }
         }

         // Refresh price so that all discounted prices are properly applied
         LoadLineItems(objQuote);
      }

      public void DebugMessage(string strMessage)
      {
         TimeSpan ts = DateTime.Now.Subtract(m_dtPerformanceTimer);
         string strTemp = ts.TotalSeconds + " sec - " + strMessage + "\r\n";

         Debug.Write(strTemp);

         if (m_objWebTrace != null)
         {
            if (m_objWebTrace.IsEnabled)
            {
               m_objWebTrace.Write("QuoteAdvantage.Engine", strMessage);
            }
         }
      }

      #region Private Methods

      private Guid InternalGetCallerId(HttpContext context)
      {
         DebugMessage("Engine.InternalGetCallerId()");
         Guid empty = Guid.Empty;
         IPrincipal user = context.User;
         if (user != null)
         {
            IIdentity identity = user.Identity;
            if (identity != null)
            {
               PropertyInfo property = identity.GetType().GetProperty("UserId");
               if (property != null)
               {
                  empty = (Guid)property.GetValue(identity, BindingFlags.GetProperty, null, new object[0], CultureInfo.InvariantCulture);
                  DebugMessage("Engine.InternalGetCallerId: UserId: " + empty.ToString());
               }
            }
         }

         if (empty == Guid.Empty)
         {
            DebugMessage("Engine.InternalGetCallerId: UserId not found in the current context");
         }

         return empty;
      }

      private CrmAuthenticationToken QuoteAdvantageExtractCrmAuthenticationToken(HttpContext context, string orgName)
      {
         DebugMessage("Engine.QuoteAdvantageExtractCrmAuthenticationToken()");
         CrmAuthenticationToken token = new CrmAuthenticationToken();
         token.AuthenticationType = 0;
         token.CrmTicket = string.Empty;
         token.OrganizationName = orgName;
         token.CallerId = InternalGetCallerId(context);

         if (token.CallerId == Guid.Empty)
         {
            // Need to login with credentials from the web.config file
            DebugMessage("Engine.QuoteAdvantageExtractCrmAuthenticationToken: Failed to load token");
         }

         return token;
      }

      private void Connect()
      {
         DebugMessage("Engine.Connect()");

         bool bIsDeveloper = false;
         bool bIsOffline = false;
         string strUserName = "";
         string strPassword = "";

         strUserName = ConfigurationManager.AppSettings["MSCRM_UserID"];
         strPassword = ConfigurationManager.AppSettings["MSCRM_Password"];

         if (!string.IsNullOrEmpty(strUserName) || !string.IsNullOrEmpty(strPassword))
         {
            bIsDeveloper = true;
         }

         if (HttpContext.Current.Request.Url.Host.ToString() == "127.0.0.1")
         {
            bIsOffline = true;
         }

         if (bIsDeveloper)
         {
            ConnectDeveloper(strUserName, strPassword);
         }
         else if (bIsOffline)
         {
            ConnectOffline();
         }
         else
         {
            ConnectOnline();
         }
      }

      private void ConnectOnline()
      {
         try
         {
            string orgname;
            HttpRequest objRequest = HttpContext.Current.Request;

            DebugMessage("Engine.ConnectOnline: Request.Url: " + objRequest.Url.ToString());

            //Retrieve the Query String from the current URL
            if (objRequest.QueryString["orgname"] == null)
            {
               orgname = string.Empty;
            }
            else
            {
               //Query String
               string orgquerystring = objRequest.QueryString["orgname"].ToString();
               if (string.IsNullOrEmpty(orgquerystring))
               {
                  orgname = string.Empty;
               }
               else
               {
                  orgname = orgquerystring;
               }
            }

            DebugMessage("Engine.ConnectOnline: orgame: " + orgname);

            m_strWebServiceUrl = GetCrmServiceForOrganization(orgname);
            DebugMessage("Engine.ConnectOnline: Connecting to: " + m_strWebServiceUrl);

            DebugMessage("Engine.ConnectOnline: Initializing token 1");

            using (new Microsoft.Crm.Sdk.CrmImpersonator())
            {
               DebugMessage("Engine.ConnectOnline: Initializing token 2");

               CrmAuthenticationToken token;
               token = QuoteAdvantageExtractCrmAuthenticationToken(HttpContext.Current, orgname);

               DebugMessage("Engine.ConnectOnline: Initializing token 3");

               token.OrganizationName = orgname;
               token.AuthenticationType = 0;

               DebugMessage("Engine.ConnectOnline: Initializing token 4");

               //Create the Service
               m_objService = new CrmService();
               m_objService.Credentials = System.Net.CredentialCache.DefaultCredentials;
               m_objService.CrmAuthenticationTokenValue = token;
               m_objService.Url = m_strWebServiceUrl;

               DebugMessage("Engine.ConnectOnline: Connecting...");

               WhoAmIRequest objUserRequest = new WhoAmIRequest();
               m_objUserIdentity = (WhoAmIResponse)m_objService.Execute(objUserRequest);

               DebugMessage("Engine.ConnectOnline: UserId:" + m_objUserIdentity.UserId);
               DebugMessage("Engine.ConnectOnline: OrganizationId:" + m_objUserIdentity.OrganizationId);
            }
         }
         catch (Exception ex)
         {
            DebugMessage("Throwing engine exception...");
            throw new ProductConfigException("Unable to connect to Microsoft CRM. Url: " + m_strWebServiceUrl, ex);
         }

      }

      private void ConnectOffline()
      {
         // UNTESTED!

         string orgname;
         HttpRequest objRequest = HttpContext.Current.Request;
         TraceContext objTrace = HttpContext.Current.Trace;

         DebugMessage("Engine.ConnectOffline: Request.Url: " + objRequest.Url.ToString());

         //Retrieve the Port and OrgName from the Registry
         RegistryKey regkey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\MSCRMClient");
         orgname = regkey.GetValue("ClientAuthOrganizationName").ToString();
         string portnumber = regkey.GetValue("CassiniPort").ToString();

         //Construct the URLs
         string baseurl = "http://localhost:" + portnumber + "/mscrmservices/2007/";
         m_strWebServiceUrl = baseurl + "crmservice.asmx";

         DebugMessage("Engine.ConnectOffline: Connecting to: " + m_strWebServiceUrl);

         using (new Microsoft.Crm.Sdk.CrmImpersonator())
         {
            CrmAuthenticationToken token;

            token = new CrmAuthenticationToken();
            token.OrganizationName = orgname;
            token.AuthenticationType = 0;

            //Create the Service
            m_objService = new CrmService();
            m_objService.Credentials = System.Net.CredentialCache.DefaultCredentials;
            m_objService.CrmAuthenticationTokenValue = token;
            m_objService.Url = m_strWebServiceUrl;

            DebugMessage("Engine.ConnectOffline: Connecting...");

            WhoAmIRequest objUserRequest = new WhoAmIRequest();
            m_objUserIdentity = (WhoAmIResponse)m_objService.Execute(objUserRequest);

            DebugMessage("Engine.ConnectOffline: UserId:" + m_objUserIdentity.UserId);
            DebugMessage("Engine.ConnectOffline: OrganizationId:" + m_objUserIdentity.OrganizationId);
         }
      }

      private void ConnectDeveloper(string strUserName, string strPassword)
      {
         m_dtPerformanceTimer = DateTime.Now;
         DebugMessage("Engine.ConnectDeveloper()");

         m_strWebServiceUrl = ConfigurationManager.AppSettings["QuoteAdvantage.CrmSdk.CrmService"];
         DebugMessage("Connecting to: " + m_strWebServiceUrl);

         string strServerName = "";

         try
         {
            m_objService = new CrmService();
            m_objService.Url = m_strWebServiceUrl;

            // Instantiate a System.Net.CredentialCache object.
            System.Net.CredentialCache credentialCache = new System.Net.CredentialCache();

            // Instantiate a System.Net.NetworkCredential object passing in the Active Directory UserName,
            // Password and Domain that will be used to authenticate into the Microsoft CRM Security Model.
            // Set up this user in Microsoft CRM and give the user the appropriate License Keys and Roles.
            System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(
               strUserName,
               strPassword,
               m_objSettings.Domain);

            // Add a value to the CredentialCache to indicate the URI of the Microsoft CRM Server, "NTLM",
            // which represents integrated Windows authenticated and the credentials of
            // the user who is to be used for authentication.
            Uri objUri = new Uri(m_strWebServiceUrl);
            strServerName = objUri.GetLeftPart(UriPartial.Authority);
            credentialCache.Add(new Uri(strServerName), "NTLM", credentials);
            m_objCredentials = credentialCache;

            DebugMessage("CRM Login Name = " + strUserName);

            CrmAuthenticationToken objToken = new CrmAuthenticationToken();
            objToken.AuthenticationType = 0;
            string strOrganizationName = HttpContext.Current.Request.QueryString["orgname"];
            objToken.OrganizationName = strOrganizationName;

            m_objService.CrmAuthenticationTokenValue = objToken;
            m_objService.Credentials = m_objCredentials;

            WhoAmIRequest objRequest = new WhoAmIRequest();
            m_objUserIdentity = (WhoAmIResponse)m_objService.Execute(objRequest);
         }
         catch (Exception ex)
         {
            DebugMessage("Throwing engine exception...");
            throw new ProductConfigException("Unable to connect to Microsoft CRM. Url: " + m_strWebServiceUrl + "   Server Name: " + strServerName + "   UserName: " + strUserName, ex);
         }
      }

      private ICredentials CreateCredentials(string strUrl, string strUserName, string strPassword, string strDomain)
      {
         // Instantiate a System.Net.CredentialCache object.
         System.Net.CredentialCache credentialCache = new System.Net.CredentialCache();

         // Instantiate a System.Net.NetworkCredential object passing in the Active Directory UserName,
         // Password and Domain that will be used to authenticate into the Microsoft CRM Security Model.
         // Set up this user in Microsoft CRM and give the user the appropriate License Keys and Roles.
         System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(
            strUserName,
            strPassword,
            strDomain);

         // Add a value to the CredentialCache to indicate the URI of the Microsoft CRM Server, "NTLM",
         // which represents integrated Windows authenticated and the credentials of
         // the user who is to be used for authentication.
         Uri objUri = new Uri(strUrl);
         string strServerName = objUri.GetLeftPart(UriPartial.Authority);
         credentialCache.Add(new Uri(strServerName), "NTLM", credentials);

         return credentialCache;
      }

      private string GetCrmServiceForOrganization(string strOrganizationName)
      {
         DebugMessage("GetCrmServiceForOrganization: Organization Name: " + strOrganizationName);
         string strResult = "";

         CrmDiscoveryService objDiscovery = new CrmDiscoveryService();


         string strDomain = ConfigurationManager.AppSettings["CrmDiscovery_Domain"];
         string strUserName = ConfigurationManager.AppSettings["CrmDiscovery_User"];
         string strPassword = ConfigurationManager.AppSettings["CrmDiscovery_Password"];

         objDiscovery.Credentials = CreateCredentials(objDiscovery.Url, strUserName, strPassword, strDomain);
         DebugMessage("GetCrmServiceForOrganization: Discovery Service URL: " + objDiscovery.Url);

         RetrieveOrganizationsRequest objOrgRequest = new RetrieveOrganizationsRequest();
         RetrieveOrganizationsResponse objOrgResponse = (RetrieveOrganizationsResponse)objDiscovery.Execute(objOrgRequest);

         foreach (OrganizationDetail objDetail in objOrgResponse.OrganizationDetails)
         {
            if (objDetail.OrganizationName == strOrganizationName)
            {
               strResult = objDetail.CrmServiceUrl;
            }
         }

         if (strResult == "")
         {
            DebugMessage("Critical Error: Organization Name was not matched for user: " + strUserName);
            throw new ApplicationException("Critical Error: Organization Name was not matched for user: " + strUserName);
         }

         DebugMessage("GetCrmServiceForOrganization: Discovery Service responded with URL: " + strResult);

         return strResult;
      }

      // Map a category ID to the name of a category in the rule file
      // This method only searches root level categories
      private string GetCategoryName(string strCategoryId, XmlNode nodeRoot)
      {
         string strTargetCategoryId = strCategoryId.ToUpper();
         string strCategoryName = "";

         foreach (XmlNode nodeCategory in nodeRoot.ChildNodes)
         {
            if (nodeCategory.Attributes["categoryId"].Value.ToUpper() == strTargetCategoryId)
            {
               strCategoryName = nodeCategory.Attributes["name"].Value;
            }
         }

         return strCategoryName;
      }

      // Perform a recursive search for a specific category node
      public XmlNode GetCategoryNode(string strCategoryId, XmlNode nodeRoot)
      {
         string strTargetCategoryId = strCategoryId.ToUpper();
         XmlNode objNode = null;

         foreach (XmlNode nodeCategory in nodeRoot.ChildNodes)
         {
            if (objNode == null)  // Already found a match?
            {
               if (nodeCategory.Name == "Category")
               {
                  if (nodeCategory.Attributes["categoryId"].Value.ToUpper() == strTargetCategoryId)
                  {
                     objNode = nodeCategory;
                  }
                  else
                  {
                     objNode = GetCategoryNode(strCategoryId, nodeCategory);
                  }
               }
            }
         }

         return objNode;
      }

      // Use a category Id to generate a full pathname for a category
      public string GetCategoryPath(string strCategoryId, XmlNode nodeRoot)
      {
         XmlNode objCategoryNode = GetCategoryNode(strCategoryId, nodeRoot);
         string strPath = "";

         // Navigate backwards up the tree to build the category path 
         while (objCategoryNode != null)
         {
            if (objCategoryNode.Name == "Category")
            {
               if (strPath == "")
               {
                  strPath = objCategoryNode.Attributes["name"].Value;
               }
               else
               {
                  strPath = objCategoryNode.Attributes["name"].Value + "/" + strPath;
               }
            }
            objCategoryNode = objCategoryNode.ParentNode;
         }

         return strPath;
      }

      public XmlNode GetProductNode(XmlNode nodeCategoryRoot, string strCategoryId, string strProductNumber)
      {
         XmlNode nodeResult = null;
         XmlNode nodeCategory = nodeCategoryRoot.SelectSingleNode("//Category[@categoryId='" + strCategoryId + "']");
         string strTargetProduct = strProductNumber.ToUpper();

         if (nodeCategory != null)
         {
            foreach (XmlNode nodeItem in nodeCategory.ChildNodes)
            {
               if (nodeItem.NodeType == XmlNodeType.Element)
               {
                  if (nodeItem.Name == "Item")
                  {
                     if (nodeItem.Attributes["productId"].Value.ToUpper() == strTargetProduct)
                     {
                        nodeResult = nodeItem;
                        break;
                     }
                  }
               }
            }
         }

         return nodeResult;
      }

      // Retrieve a unit of measure entry from the XML file
      public void ApplyUnitOfMeasure(XmlNode nodeCategoryRoot, Product objProduct, Organization objOrganization)
      {
         XmlNode nodeProduct = GetProductNode(nodeCategoryRoot, objProduct.CategoryId, objProduct.ProductNumber);

         objProduct.UnitOfMeasureId = objProduct.DefaultUnitOfMeasureId;

         if (nodeProduct != null)
         {
            if (nodeProduct.Attributes["uom"] != null)
            {
               if (nodeProduct.Attributes["uom"].Value != "")
               {
                  Guid guidUnitOfMeasure = new Guid(nodeProduct.Attributes["uom"].Value);

                  // Verify that the UOM ID is valid
                  string strUom = LoadUnitOfMeasure(guidUnitOfMeasure, objOrganization);

                  if (strUom != "")
                  {
                     objProduct.UnitOfMeasureId = guidUnitOfMeasure;
                  }
               }
            }
         }
      }

      public string LoadUnitOfMeasure(Guid unitOfMeasureId, Organization objOrganization)
      {
         string strResult = "";

         if (unitOfMeasureId != Guid.Empty)
         {
            uom objUom = (uom)m_objUnitOfMeasureCache[unitOfMeasureId];

            if (objUom == null)
            {
               // Load the unit of measure table from the database
               ColumnSet objColumns = new ColumnSet();
               objColumns.Attributes = new string[] { "uomid", "uomscheduleid", "name", "quantity" };

               // Create a ConditionExpression.
               ConditionExpression conditionPrincipal1 = new ConditionExpression();

               conditionPrincipal1.AttributeName = "organizationid";
               conditionPrincipal1.Operator = ConditionOperator.Equal;
               conditionPrincipal1.Values = new object[1];
               conditionPrincipal1.Values[0] = objOrganization.OrganizationId;

               // Create the FilterExpression.
               FilterExpression filterPrincipal = new FilterExpression();

               // Set the properties of the FilterExpression.
               filterPrincipal.FilterOperator = LogicalOperator.And;
               filterPrincipal.Conditions = new ConditionExpression[] { conditionPrincipal1 };

               // Create the QueryExpression.
               QueryExpression queryPrincipal = new QueryExpression();

               // Set the properties of the QueryExpression.
               queryPrincipal.EntityName = EntityName.uom.ToString();
               queryPrincipal.ColumnSet = objColumns;
               queryPrincipal.Criteria = filterPrincipal;

               BusinessEntityCollection objList = m_objService.RetrieveMultiple(queryPrincipal);

               m_objUnitOfMeasureCache.Clear();

               if (objList.BusinessEntities.Length > 0)
               {
                  foreach (BusinessEntity objItem in objList.BusinessEntities)
                  {
                     uom objUnit = (uom)objItem;
                     m_objUnitOfMeasureCache.Add(objUnit.uomid.Value, objUnit);
                  }
               }

               objUom = (uom)m_objUnitOfMeasureCache[unitOfMeasureId];
            }

            if (objUom != null)
            {
               strResult = objUom.name;
            }
         }

         return strResult;
      }

      private void LoadQuoteHeader(Guid quoteId, Quote objQuote)
      {
         DebugMessage("LoadQuoteHeader() Service URL: " + m_objService.Url);
         using (new Microsoft.Crm.Sdk.CrmImpersonator())
         {
            ColumnSet objColumns = new ColumnSet();
            objColumns.Attributes = new string[] { "quoteid", "quotenumber", "name", "totalamount", "pricelevelid", "statecode", "totalamount" };
            quote objCrmQuote = (quote)m_objService.Retrieve(EntityName.quote.ToString(), quoteId, objColumns);
            objQuote.LoadFromCrmQuote(objCrmQuote);
         }
      }

      // Load all of the line items for a quote
      public void LoadLineItems(Quote objQuote)
      {
         try
         {
            DebugMessage("LoadLineItems()");

            using (new Microsoft.Crm.Sdk.CrmImpersonator())
            {
               ColumnSet objColumns = new ColumnSet();
               objColumns.Attributes = new string[] 
                  {
                     "qadv_config_parent_id", 
                     "qadv_config_category_id", 
                     "qadv_allow_discounting", 
                     "qadv_percentdiscount",
                     "qadv_percentmarkup",                          
                     "lineitemnumber", 
                     "productid", 
                     "productdescription",
                     "priceperunit",
                     "extendedamount",
                     "quantity",
                     "quoteid",
                     "quotedetailid",
                     "manualdiscountamount",
                     "tax",
                     "baseamount",
                     "uomid"            
                  };

               // Create a ConditionExpression.
               ConditionExpression conditionPrincipal = new ConditionExpression();

               // Set the ConditionExpressions properties so that the condition is
               // true when the ownerid of the account equals the principalId.
               conditionPrincipal.AttributeName = "quoteid";
               conditionPrincipal.Operator = ConditionOperator.Equal;
               conditionPrincipal.Values = new object[1];
               conditionPrincipal.Values[0] = objQuote.QuoteId;

               // Create the FilterExpression.
               FilterExpression filterPrincipal = new FilterExpression();

               // Set the properties of the FilterExpression.
               filterPrincipal.FilterOperator = LogicalOperator.And;
               filterPrincipal.Conditions = new ConditionExpression[] { conditionPrincipal };

               // Create the QueryExpression.
               QueryExpression queryPrincipal = new QueryExpression();

               // Set the properties of the QueryExpression.
               queryPrincipal.EntityName = EntityName.quotedetail.ToString();
               queryPrincipal.ColumnSet = objColumns;
               queryPrincipal.Criteria = filterPrincipal;

               // Sort order
               OrderExpression oe1 = new OrderExpression();
               oe1.AttributeName = "lineitemnumber";
               oe1.OrderType = OrderType.Ascending;
               queryPrincipal.Orders = new OrderExpression[] { oe1 };

               // Create the request object.
               RetrieveMultipleRequest retrievePrincipal = new RetrieveMultipleRequest();

               // Set the properties of the request object.
               retrievePrincipal.Query = queryPrincipal;

               // Execute the request.
               RetrieveMultipleResponse objResponse = (RetrieveMultipleResponse)m_objService.Execute(retrievePrincipal);
               objQuote.LoadQuoteDetails(objResponse.BusinessEntityCollection);
               LoadQuoteProducts(objQuote);
            }
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);
            throw new ApplicationException("Unable to load quote line items.  " + strMessage);
         }

      }

      // For each line item on a quote, retrieve the corresponding Product and apply its
      // product number and custom fields to the line item
      public void LoadQuoteProducts(Quote objQuote)
      {
         foreach (QuoteDetail objLineItem in objQuote.LineItems)
         {
            if (objLineItem.ProductId == Guid.Empty)
            {
               // This is a write-in product
               objLineItem.Name = objLineItem.ProductDescription;
            }
            else
            {
               Product objProduct = LoadProduct(objLineItem.ProductId, objQuote.PriceLevelId);
               objLineItem.ProductNumber = objProduct.ProductNumber;
               objLineItem.Name = objProduct.Name;
               objLineItem.ProductTypeCode = objProduct.ProductTypeCode;
               objLineItem.ProductCustomField1 = objProduct.CustomField1;
               objLineItem.ProductCustomField2 = objProduct.CustomField2;
            }
         }
      }

      public void ValidateCategory(XmlNode nodeCategory, QuoteDetail objItem)
      {
         bool bCategorySatisfied = false;
         string strRuleProductNumber = "";
         string strCategoryId = nodeCategory.Attributes["categoryId"].Value;
         string strCategoryType = "SingleSelect";
         bool bAllowEmpty = false;

         if (nodeCategory.Attributes["type"] != null)
         {
            strCategoryType = nodeCategory.Attributes["type"].Value;
         }

         if (strCategoryType == "MultiSelect")
            bAllowEmpty = true; // Multi-select categories can be left empty

         // Step through every item in the category and look for a matching child line item on the QuoteDetail
         foreach (XmlNode nodeCategoryItem in nodeCategory.ChildNodes)
         {
            if (nodeCategoryItem.NodeType == XmlNodeType.Element)
            {
               string strProductID = "";

               if (nodeCategoryItem.Name == "EmptyItem")
               {
                  bAllowEmpty = true;  // It's OK if no choice was made in this category
               }
               else if (nodeCategoryItem.Name == "Item")
               {
                  strProductID = nodeCategoryItem.Attributes["productId"].Value;

                  ArrayList objMatchingItemList = objItem.FindChildItemList(strProductID, strCategoryId);

                  foreach (QuoteDetail objMatchingLineItem in objMatchingItemList)
                  {
                     // We have found the item that was picked from this category. Make sure
                     // that all of this item's category rules are valid
                     if (nodeCategoryItem.ChildNodes.Count > 0)
                     {
                        foreach (XmlNode nodeRule in nodeCategoryItem.ChildNodes)
                        {
                           if (nodeRule.NodeType == XmlNodeType.Element)
                           {
                              switch (nodeRule.Name)
                              {
                                 case "RequiresCategory":
                                    ProcessRequiresCategoryRule(nodeRule, objItem, objMatchingLineItem);
                                    bCategorySatisfied = true;
                                    break;

                                 case "RequiresItem":
                                    strRuleProductNumber = nodeRule.InnerText;
                                    ProcessRequiresItemRule(strRuleProductNumber, objItem, objMatchingLineItem);
                                    bCategorySatisfied = true;
                                    break;

                                 case "NotCompatibleWith":
                                    strRuleProductNumber = nodeRule.InnerText;
                                    ProcessNotCompatibleWithRule(strRuleProductNumber, objItem, objMatchingLineItem);
                                    bCategorySatisfied = true;
                                    break;

                                 case "LinkedItem":
                                    // Nothing needs to be done here.
                                    break;

                                 default:
                                    throw new ProductConfigException("Unknown rule '" + nodeRule.Name + "' on item '" + strProductID + "'");
                              }
                           }
                        } // end foreach
                     }
                     else
                     {
                        bCategorySatisfied = true;  // No rules were found for this item, so it passes
                     }
                     break;
                  }
               }
            }
         } // end foreach

         if (bCategorySatisfied == false && (bAllowEmpty == false))
         {
            string strCategoryName = nodeCategory.Attributes["name"].Value;
            string strMessage = LoadString("msgMustSelectItem", strCategoryName, "");

            ProductConfigException ex = new ProductConfigException(strMessage);
            ex.OwningProduct = objItem;
            ex.CategoryId = nodeCategory.Attributes["categoryId"].Value;
            throw ex;
         }
      }

      private string LoadString(string strName, string strParam1, string strParam2)
      {
         string strMessage = m_objResourceManager.GetString(strName);

         if (strParam1.Length > 0)
            strMessage = strMessage.Replace("%1", strParam1);

         if (strParam2.Length > 0)
            strMessage = strMessage.Replace("%2", strParam2);

         return strMessage;
      }

      private void ProcessNotCompatibleWithRule(string strProductNumber, QuoteDetail objParentItem, QuoteDetail objChildItem)
      {
         if (objParentItem.FindChildItem(strProductNumber) != null)
         {
            Product objProduct = this.LoadProductFromProductNumber(strProductNumber, objParentItem.QuoteOwner.PriceLevelId);
            string strProductName = objProduct.Name;
            string strMessage = LoadString("msgNotCompatible", strProductName, "");
            string strMessage2 = LoadString("msgValidationFailed", objChildItem.Name, "");
            ProductConfigException ex = new ProductConfigException(strMessage2 + " " + strMessage);
            ex.OwningProduct = objParentItem;
            ex.MisconfiguredItem = objChildItem;
            ex.CategoryId = objChildItem.CategoryId;
            throw ex;
         }
      }

      private void ProcessRequiresCategoryRule(XmlNode nodeRule, QuoteDetail objParentItem, QuoteDetail objChildItem)
      {
         string strCategoryId = nodeRule.InnerText;
         XmlNode nodeRoot = nodeRule.OwnerDocument.DocumentElement;

         if (objParentItem.QuoteOwner.FindCategoryItem(objParentItem, strCategoryId) == null)
         {
            //string strCategoryName = nodeRule.ParentNode.ParentNode.Attributes["name"].Value;
            string strCategoryName = GetCategoryName(strCategoryId, nodeRoot);
            string strMessage = LoadString("msgValidationFailed", objChildItem.Name, "");
            string strMessage2 = LoadString("msgRequiresCategory", objChildItem.Name, strCategoryName);

            ProductConfigException ex = new ProductConfigException(strMessage + " " + strMessage2);
            ex.OwningProduct = objParentItem;
            ex.MisconfiguredItem = objChildItem;
            ex.CategoryId = objChildItem.CategoryId;
            throw ex;
         }
      }

      private void ProcessRequiresItemRule(string strProductNumber, QuoteDetail objParentItem, QuoteDetail objChildItem)
      {
         if (objParentItem.FindChildItem(strProductNumber) == null)
         {
            string strMessage;
            Product objProduct = this.LoadProductFromProductNumber(strProductNumber, objParentItem.QuoteOwner.PriceLevelId);

            if (objProduct == null)
            {

               strMessage = "Rule File Error: The RequiresItem rule in the " + objChildItem.CategoryId + " category could not be processed because an item with a product number = " + strProductNumber + " could not be loaded.";
               throw new ApplicationException(strMessage);
            }
            else
            {
               string strProductName = objProduct.Name;
               strMessage = LoadString("msgValidationFailed", objChildItem.Name, "");
               string strMessage2 = LoadString("msgRequiresItem", strProductName, "");

               ProductConfigException ex = new ProductConfigException(strMessage + " " + strMessage2);
               ex.OwningProduct = objParentItem;
               ex.MisconfiguredItem = objChildItem;
               ex.CategoryId = objChildItem.CategoryId;
               throw ex;
            }
         }
      }

      // Update a quote detail item that is already saved in the database
      public void UpdateQuoteDetail(QuoteDetail objQuoteDetail)
      {
         try
         {
            quotedetail objCrmQuoteDetail = objQuoteDetail.CreateCrmObject();
            m_objService.Update(objCrmQuoteDetail);
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);

            ProductConfigException ex = new ProductConfigException("Unable to Update Quote Detail. " + strMessage, err);
            throw ex;
         }
      }

      // Update a quote header that is already saved in the database
      public void UpdateQuote(Quote objQuote)
      {
         try
         {
            quote objCrmQuote = objQuote.CreateCrmObject();
            m_objService.Update(objCrmQuote);
         }
         catch (System.Web.Services.Protocols.SoapException err)
         {
            string strMessage = String.Concat("ErrorMessage: ", err.Message, " ", err.Detail.OuterXml, " Source: ", err.Source);

            ProductConfigException ex = new ProductConfigException("Unable to Update Quote. " + strMessage, err);
            throw ex;
         }
      }

      #endregion

   } // end class
}
